============================
SPY CAM — FULL CODE BUNDLE
============================

[1] Arduino — Camera3.ino
-------------------------
// ===== Camera3.ino — ESP32-CAM SPY Cam (30s interval, flash OFF) =====
#include "esp_camera.h"
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <Client.h>

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

extern "C" { #include "img_converters.h" }

// ---------- USER CONFIG ----------
const char* WIFI_SSID = "";
const char* WIFI_PASS = "";

const char* HOST = "prasadjagdale.com";
const int   PORT = 443;                        // 443 HTTPS
const char* PATH = "/SupportingFiles/db.php";  // upload endpoint
bool        USE_HTTPS = true;                  // set false if TLS fails

const uint32_t SHOT_INTERVAL_MS = 30UL * 1000UL;

// ---------- OLED SH1106 ----------
#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
#define OLED_ADDR     0x3C
#define SDA_PIN       15
#define SCL_PIN       14
Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ---------- Camera Pins (AI Thinker) ----------
#define PWDN_GPIO_NUM   32
#define RESET_GPIO_NUM  -1
#define XCLK_GPIO_NUM    0
#define SIOD_GPIO_NUM   26
#define SIOC_GPIO_NUM   27
#define Y9_GPIO_NUM     35
#define Y8_GPIO_NUM     34
#define Y7_GPIO_NUM     39
#define Y6_GPIO_NUM     36
#define Y5_GPIO_NUM     21
#define Y4_GPIO_NUM     19
#define Y3_GPIO_NUM     18
#define Y2_GPIO_NUM      5
#define VSYNC_GPIO_NUM  25
#define HREF_GPIO_NUM   23
#define PCLK_GPIO_NUM   22

// ---------- Globals ----------
int g_camW = 0, g_camH = 0;
uint32_t g_lastShot = 0;
uint32_t g_shotCount = 0;

// ---------- OLED helpers ----------
void oledCenterText(const String& msg){
  display.clearDisplay(); display.setTextSize(1); display.setTextColor(SH110X_WHITE);
  int16_t x1,y1; uint16_t w,h; display.getTextBounds(msg,0,0,&x1,&y1,&w,&h);
  int x=(SCREEN_WIDTH-w)/2; if(x<0)x=0; int y=(SCREEN_HEIGHT-h)/2; if(y<0)y=0;
  display.setCursor(x,y); display.print(msg); display.display();
}
void oledBigCenter(const char* title,int hold_ms=1200){
  display.clearDisplay(); display.setTextColor(SH110X_WHITE); int size=2;
  for(;;){ display.setTextSize(size); int16_t x1,y1; uint16_t w,h;
    display.getTextBounds(title,0,0,&x1,&y1,&w,&h);
    if(w<=SCREEN_WIDTH-4||size<=1){ int x=(SCREEN_WIDTH-w)/2; int y=(SCREEN_HEIGHT-h)/2;
      display.setCursor(x,y); display.print(title); display.display(); break; }
    size--;
  } if(hold_ms>0) delay(hold_ms);
}
void oledSmallStatus(const String& l1,const String& l2=""){
  display.clearDisplay(); display.setTextSize(1); display.setTextColor(SH110X_WHITE);
  display.setCursor(4,18); display.print(l1); display.setCursor(4,34); display.print(l2); display.display();
}

// ---------- Camera ----------
bool cameraBegin(int &outW,int &outH){
  camera_config_t c; c.ledc_channel=LEDC_CHANNEL_0; c.ledc_timer=LEDC_TIMER_0;
  c.pin_d0=Y2_GPIO_NUM; c.pin_d1=Y3_GPIO_NUM; c.pin_d2=Y4_GPIO_NUM; c.pin_d3=Y5_GPIO_NUM;
  c.pin_d4=Y6_GPIO_NUM; c.pin_d5=Y7_GPIO_NUM; c.pin_d6=Y8_GPIO_NUM; c.pin_d7=Y9_GPIO_NUM;
  c.pin_xclk=XCLK_GPIO_NUM; c.pin_pclk=PCLK_GPIO_NUM; c.pin_vsync=VSYNC_GPIO_NUM; c.pin_href=HREF_GPIO_NUM;
  c.pin_sccb_sda=SIOD_GPIO_NUM; c.pin_sccb_scl=SIOC_GPIO_NUM; c.pin_pwdn=PWDN_GPIO_NUM; c.pin_reset=RESET_GPIO_NUM;
  c.xclk_freq_hz=20000000; c.pixel_format=PIXFORMAT_RGB565;  // rotate in RAM

  if(psramFound()){ c.frame_size=FRAMESIZE_VGA; outW=640; outH=480; c.fb_count=1; }
  else { c.frame_size=FRAMESIZE_QVGA; outW=320; outH=240; c.fb_count=1; }

  if(esp_camera_init(&c)!=ESP_OK){ Serial.println("Camera init failed"); return false; }
  sensor_t* s=esp_camera_sensor_get(); s->set_vflip(s,0); s->set_hmirror(s,0);
  return true;
}

// ---------- Rotate 90° CW (RGB565) ----------
void rotate90CW_rgb565(const uint8_t* src,int srcW,int srcH,uint8_t* dst){
  const uint16_t* s=(const uint16_t*)src; uint16_t* d=(uint16_t*)dst;
  for(int y=0;y<srcH;y++){ for(int x=0;x<srcW;x++){
    uint16_t pix=s[y*srcW+x]; int xp=y; int yp=(srcW-1-x); d[yp*srcH+xp]=pix; } }
}

// ---------- Multipart body (C++11) ----------
bool sendMultipartBody(Client& cli,const String& head,const uint8_t* data,size_t len,const String& tail){
  if(!cli.print(head)) return false;
  const size_t CHUNK=2048; size_t off=0;
  while(off<len){ size_t n=(len-off)>CHUNK?CHUNK:(len-off); size_t w=cli.write(data+off,n); if(w!=n) return false; off+=w; }
  if(!cli.print(tail)) return false; return true;
}

// ---------- Upload ----------
bool uploadJPEG(const uint8_t* data,size_t len){
  char fname[48]; snprintf(fname,sizeof(fname),"spy_%lu.jpg",(unsigned long)millis());
  String boundary="----ESP32CamBoundary";
  String head="--"+boundary+"\r\n"
              "Content-Disposition: form-data; name=\"image\"; filename=\""+String(fname)+"\"\r\n"
              "Content-Type: image/jpeg\r\n\r\n";
  String tail="\r\n--"+boundary+"--\r\n";
  size_t contentLength=head.length()+len+tail.length();

  Serial.printf("Uploading %u bytes as %s\n",(unsigned)len,fname);

  if(USE_HTTPS){
    WiFiClientSecure cli; cli.setInsecure(); cli.setTimeout(20000);
    if(!cli.connect(HOST,PORT)){ Serial.println("HTTPS connect failed"); return false; }
    cli.printf("POST %s HTTP/1.1\r\n",PATH);
    cli.printf("Host: %s\r\n",HOST);
    cli.printf("Content-Type: multipart/form-data; boundary=%s\r\n",boundary.c_str());
    cli.printf("Content-Length: %u\r\n",(unsigned)contentLength);
    cli.print("Connection: close\r\n\r\n");
    if(!sendMultipartBody(cli,head,data,len,tail)){ cli.stop(); return false; }
    while(cli.connected()){ while(cli.available()) Serial.write(cli.read()); }
    cli.stop(); Serial.println("\n<- END"); return true;
  }else{
    WiFiClient cli; cli.setTimeout(20000);
    if(!cli.connect(HOST,80)){ Serial.println("HTTP connect failed"); return false; }
    cli.printf("POST %s HTTP/1.1\r\n",PATH);
    cli.printf("Host: %s\r\n",HOST);
    cli.printf("Content-Type: multipart/form-data; boundary=%s\r\n",boundary.c_str());
    cli.printf("Content-Length: %u\r\n",(unsigned)contentLength);
    cli.print("Connection: close\r\n\r\n");
    if(!sendMultipartBody(cli,head,data,len,tail)){ cli.stop(); return false; }
    while(cli.connected()){ while(cli.available()) Serial.write(cli.read()); }
    cli.stop(); Serial.println("\n<- END"); return true;
  }
}

// ---------- Capture → rotate → encode → upload ----------
bool captureRotateUploadOnce(){
  oledSmallStatus("Capturing...","");
  camera_fb_t* fb=esp_camera_fb_get(); if(!fb){ oledCenterText("Capture Failed"); delay(800); return false; }
  if(fb->format!=PIXFORMAT_RGB565) Serial.println("Warn: pixel format != RGB565");

  size_t rotated_len=(size_t)g_camH*(size_t)g_camW*2;
  uint8_t* rotated=(uint8_t*)ps_malloc(rotated_len);
  if(!rotated){ oledCenterText("Mem Error"); esp_camera_fb_return(fb); delay(800); return false; }

  oledSmallStatus("Rotating 90...","");
  rotate90CW_rgb565(fb->buf,g_camW,g_camH,rotated);

  oledSmallStatus("Encoding JPEG...","");
  uint8_t* jpg_buf=nullptr; size_t jpg_len=0;
  const int JPEG_QUALITY=12;
  bool okEnc=fmt2jpg(rotated,rotated_len,g_camH,g_camW,PIXFORMAT_RGB565,JPEG_QUALITY,&jpg_buf,&jpg_len);

  free(rotated); esp_camera_fb_return(fb);
  if(!okEnc||!jpg_buf||jpg_len==0){ oledCenterText("Encode Failed"); delay(800); return false; }

  oledSmallStatus("Uploading...","");
  bool ok=uploadJPEG(jpg_buf,jpg_len); free(jpg_buf);

  if(ok){ g_shotCount++; oledSmallStatus("Uploaded ✔",String("Total: ")+g_shotCount); }
  else { oledCenterText("Upload Failed"); }
  delay(800); return ok;
}

// ---------- Wi-Fi keepalive ----------
void ensureWiFi(){
  if(WiFi.status()==WL_CONNECTED) return;
  WiFi.disconnect(); WiFi.begin(WIFI_SSID,WIFI_PASS);
  uint32_t t0=millis(); while(WiFi.status()!=WL_CONNECTED && millis()-t0<15000) delay(300);
}

void setup(){
  Serial.begin(115200); delay(300);
  Serial.println("\n=== ESP32-CAM : SPY Cam (30s interval, flash OFF) ===");

  Wire.begin(SDA_PIN,SCL_PIN);
  display.begin(OLED_ADDR,true);
  display.setContrast(160); display.clearDisplay(); display.display();

  oledBigCenter("SPY Cam",1200);

  if(!cameraBegin(g_camW,g_camH)){ oledCenterText("Cam Init Fail"); delay(2500); ESP.restart(); }
  Serial.printf("Camera RGB565 ready: %dx%d\n",g_camW,g_camH);

  WiFi.begin(WIFI_SSID,WIFI_PASS);
  Serial.print("WiFi connecting");
  uint32_t t0=millis();
  while(WiFi.status()!=WL_CONNECTED){
    Serial.print("."); delay(500);
    if(millis()-t0>20000){ oledCenterText("WiFi Timeout"); Serial.println("\nProceeding (will retry)..."); break; }
  }
  if(WiFi.status()==WL_CONNECTED) Serial.printf("\nWiFi OK: %s\n",WiFi.localIP().toString().c_str());

  g_lastShot=millis();
  oledSmallStatus("Waiting...","Next shot in 30s");
}

void loop(){
  ensureWiFi();
  uint32_t now=millis();
  if(now - g_lastShot >= SHOT_INTERVAL_MS){
    g_lastShot = now;
    captureRotateUploadOnce();
    oledSmallStatus("Waiting...","Next shot in 30s");
  }
  delay(50);
}


[2] Server — db.php
-------------------
<?php
/*******************
 * db.php - Upload endpoint: saves JPEG blob into MySQL
 *******************/
$dbHost=""; 
$dbUser="";
$dbPass=""; 
$dbName="";
$table="images";

$mysqli = @new mysqli($dbHost, $dbUser, $dbPass, $dbName);
if ($mysqli->connect_errno) { http_response_code(500); exit("DB Error"); }
$mysqli->set_charset("binary");

// No-cache to avoid stale responses
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: 0");

if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
  http_response_code(400); exit("no image");
}

$blob = file_get_contents($_FILES['image']['tmp_name']);
$stmt = $mysqli->prepare("INSERT INTO `$table` (image) VALUES (?)");
$stmt->bind_param("b", $blob);
$stmt->send_long_data(0, $blob);
if ($stmt->execute()) {
  echo "OK id=".$stmt->insert_id;
} else {
  http_response_code(500); echo "DB insert error";
}
$stmt->close();
$mysqli->close();


[3] Server — view.php
---------------------
<?php
/*****************************
 * view.php  (Gallery + Retro + Delete + Delete All)
 *****************************/

// --- DB CONFIG ---
$dbHost     = "";
$dbUsername = "";
$dbPassword = "";
$dbName     = "";
$table      = "images";

// ---- CONNECT ----
$mysqli = @new mysqli($dbHost, $dbUsername, $dbPassword, $dbName);
if ($mysqli->connect_errno) {
    http_response_code(500);
    echo "DB connect error: " . htmlspecialchars($mysqli->connect_error);
    exit;
}
$mysqli->set_charset("binary");

/****************************************
 * 1) Serve image with retro effect: view.php?action=img&id=123
 ****************************************/
if (isset($_GET['action']) && $_GET['action'] === 'img') {
    $id = isset($_GET['id']) ? intval($_GET['id']) : 0;
    if ($id <= 0) { http_response_code(400); exit("Bad id"); }

    $stmt = $mysqli->prepare("SELECT image FROM `$table` WHERE id = ?");
    $stmt->bind_param("i", $id);
    $stmt->execute();
    $stmt->store_result();
    $stmt->bind_result($blob);

    if ($stmt->num_rows === 1 && $stmt->fetch()) {
        // serve as JPEG and disable cache
        header("Content-Type: image/jpeg");
        header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
        header("Pragma: no-cache");
        header("Expires: 0");

        // Try PHP-GD retro post-processing
        if (function_exists('imagecreatefromstring')) {
            $src = @imagecreatefromstring($blob);
            if ($src !== false) {
                imagefilter($src, IMG_FILTER_GRAYSCALE);
                imagefilter($src, IMG_FILTER_COLORIZE, 90, 60, 40);
                imagefilter($src, IMG_FILTER_CONTRAST, -10);
                imagefilter($src, IMG_FILTER_BRIGHTNESS, -5);
                imagejpeg($src, null, 90);
                imagedestroy($src);
                $stmt->close();
                exit;
            }
        }
        // fallback raw
        echo $blob;

    } else {
        http_response_code(404);
        echo "Not found";
    }
    $stmt->close();
    exit;
}

/****************************************
 * 2) POST: Delete All / Delete single
 ****************************************/
$msg = "";
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_POST['delete_all'])) {
        if ($mysqli->query("TRUNCATE TABLE `$table`")) {
            $msg = "✅ All photos deleted and counter reset.";
        } else {
            $msg = "❌ Delete all failed: " . htmlspecialchars($mysqli->error);
        }
    }
    elseif (isset($_POST['delete_id'])) {
        $delId = intval($_POST['delete_id']);
        $stmt = $mysqli->prepare("DELETE FROM `$table` WHERE id = ?");
        $stmt->bind_param("i", $delId);
        if ($stmt->execute()) {
            $msg = "🗑️ Photo #$delId deleted.";
        } else {
            $msg = "❌ Delete failed: " . htmlspecialchars($stmt->error);
        }
        $stmt->close();
    }
}

/****************************************
 * 3) Fetch list for gallery (IDs only)
 ****************************************/
$res = $mysqli->query("SELECT id FROM `$table` ORDER BY id DESC");
$rows = [];
if ($res) { while ($r = $res->fetch_assoc()) $rows[] = $r; $res->free(); }
$mysqli->close();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Retro Gallery</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  :root {
    --gap: 12px;
    --bg: #0f1115;
    --card: #161a22;
    --text: #e7e7e7;
    --muted:#9aa0a6;
    --danger:#ff4d4f;
    --danger2:#b02020;
  }
  *{box-sizing:border-box}
  body { margin:0; font-family:system-ui,Segoe UI,Roboto,Arial; background:var(--bg); color:var(--text); }
  header { padding:16px 20px; border-bottom:1px solid #202635; display:flex; gap:12px; align-items:center; justify-content:space-between;}
  h1 { margin:0; font-size:18px; font-weight:600; letter-spacing:.3px; }
  .msg { margin:14px 20px; padding:10px 12px; background:#102a12; color:#9ef29e; border:1px solid #214521; border-radius:8px; display:<?php echo $msg? 'block':'none';?>;}
  .grid { padding:18px; display:grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: var(--gap); }
  @media (max-width:1100px){ .grid{ grid-template-columns: repeat(3,1fr);} }
  @media (max-width:780px){ .grid{ grid-template-columns: repeat(2,1fr);} }
  @media (max-width:520px){ .grid{ grid-template-columns: 1fr;} }
  .card { background:var(--card); border:1px solid #202635; border-radius:14px; overflow:hidden; display:flex; flex-direction:column; }
  .thumb {
    aspect-ratio: 4/3; width:100%; object-fit:cover; background:#0b0d12; display:block;
    filter: sepia(60%) contrast(1.1) brightness(0.9) saturate(0.85) hue-rotate(-10deg);
    transition: transform .25s ease, filter .25s ease;
  }
  .thumb:hover {
    filter: sepia(80%) contrast(1.2) brightness(1.0) saturate(0.9) hue-rotate(-18deg);
    transform: scale(1.02);
  }
  .meta { padding:10px 12px; display:flex; align-items:center; justify-content:space-between; }
  .id { font-size:13px; color:var(--muted); }
  form { padding:0 12px 12px; }
  .btn-del { width:100%; padding:9px 10px; border:none; border-radius:10px; cursor:pointer; font-weight:600;
             background:linear-gradient(180deg,var(--danger),var(--danger2)); color:#fff; }
  .btn-del:hover { filter:brightness(1.05); }
  .btn-all { background:linear-gradient(180deg,#ff7b00,#d13b00); color:#fff; font-weight:600; border:none; border-radius:8px; padding:8px 14px; cursor:pointer; }
  .btn-all:hover{ filter:brightness(1.08);}
  .empty { padding:40px 20px; text-align:center; color:var(--muted); }
  .top-actions { display:flex; gap:10px; align-items:center; }
  .tip { font-size:12px; color:#9aa0a6 }
</style>
<script>
function confirmDel(id){
  if(confirm("Delete photo #"+id+" ? This cannot be undone.")){
    document.getElementById("fdel-"+id).submit();
  }
  return false;
}
function confirmAll(){
  return confirm("⚠️ Delete ALL photos and reset counter?");
}
</script>
</head>
<body>
  <header>
    <h1>Retro Gallery</h1>
    <div class="top-actions">
      <div class="tip">Server-side + CSS retro look. Delete All resets counter.</div>
      <form method="post" onsubmit="return confirmAll();">
        <input type="hidden" name="delete_all" value="1">
        <button class="btn-all" type="submit">🗑️ Delete All Photos</button>
      </form>
    </div>
  </header>

  <div class="msg"><?php echo htmlspecialchars($msg); ?></div>

  <?php if (empty($rows)): ?>
    <div class="empty">No images found.</div>
  <?php else: ?>
  <section class="grid">
    <?php $ver = time(); foreach ($rows as $r): $id=(int)$r['id']; ?>
      <article class="card">
        <img class="thumb" src="view.php?action=img&id=<?php echo $id; ?>&v=<?php echo $ver; ?>" alt="img <?php echo $id; ?>">
        <div class="meta">
          <div class="id">#<?php echo $id; ?></div>
          <a href="view.php?action=img&id=<?php echo $id; ?>&v=<?php echo $ver; ?>" target="_blank" style="text-decoration:none;color:#8ab4ff;font-size:13px;">Open</a>
        </div>
        <form id="fdel-<?php echo $id; ?>" method="post" onsubmit="return confirmDel(<?php echo $id; ?>);">
          <input type="hidden" name="delete_id" value="<?php echo $id; ?>">
          <button class="btn-del" type="submit">Delete</button>
        </form>
      </article>
    <?php endforeach; ?>
  </section>
  <?php endif; ?>
</body>
</html>

